package daybook.model.transaction;

import daybook.Resources;
import daybook.annotation.Bean;
import daybook.exception.ValidationException;
import daybook.model.Account;
import daybook.model.Book;
import daybook.model.Commodity;
import daybook.model.DC;
import daybook.util.Utils;
import org.immutables.value.Value;

import java.math.BigDecimal;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;

@Bean
@Value.Immutable
public abstract class TransactionEntry {
    // 以book的defaultCurrency为单位
    public abstract BigDecimal getAmount();
    public abstract Account getAccount();

    // 当account的commodity和默认的commodity不同时使用
    @Value.Default
    public Set<Item> getItems() {
        return Collections.emptySet();
    }

    @Value.Default
    public String getDescription() {
        return "";
    }

    @Value.Default
    public Set<String> getTags() {
        return Collections.emptySet();
    }

    public int getDc() {
        return DC.getDc(getAmount(), getAccount().getType().getNormalBalance());
    }

    public boolean isNormalBalance() {
        return DC.isNormalBalance(getAmount(), getAccount().getType().getNormalBalance());
    }

    // 不需要外部信息的校验
    public void validate() {
        validate(false);
    }

    private void validate(boolean onlySelf) {
        if (!getItems().isEmpty()) {
            final BigDecimal sum = getItems().stream()
                    .collect(Collectors.reducing(BigDecimal.ZERO, Item::getProduct, BigDecimal::add));

            if (sum.compareTo(getAmount().abs()) != 0) {
                throw ValidationException.of(Resources.ErrorMessages.INVALID_COSTS_AND_COUNTS, toPrettyString());
            }

            if (!onlySelf) {
                for (Item item : getItems()) {
                    item.validate();
                }
            }
        }
    }

    // 需要外部信息的校验
    public void validate(Commodity defaultCurrency) {
        validate(true);

        if (getAmount().scale() != defaultCurrency.getFractionDigitCount()) {
            throw ValidationException.of(Resources.ErrorMessages.INVALID_COMMODITY_FRACTION,
                    getAmount(), defaultCurrency.getFractionDigitCount());
        }

        if (!getItems().isEmpty()) {
            if (isDefaultCommodity(defaultCurrency)) {
                throw ValidationException.of(Resources.ErrorMessages.SHOULD_NOT_HAVE_COSTS_AND_COUNTS, toPrettyString());
            }

            final Commodity commodity = getAccount().getCommodity().get();
            for (Item item : getItems()) {
                item.validate(commodity, defaultCurrency);
            }
        }
    }

    public boolean isDefaultCommodity(Commodity defaultCommodity) {
        return getAccount().getCommodity()
                .map(commodity -> commodity.equals(defaultCommodity))
                .orElse(true);
    }

    public Commodity getCommodity(Commodity defaultCommodity) {
        return getAccount().getCommodity().orElse(defaultCommodity);
    }

    private String toPrettyString(String accountName) {
        final StringBuilder sb = new StringBuilder("\t");
        sb.append(accountName);

        if (!getTags().isEmpty()) {
            sb.append("[")
                    .append(String.join(", ", getTags()))
                    .append("]");
        }

        sb.append(" ").append(getAmount().signum() > 0 ? "+": "")
                .append(getAmount().toPlainString());
        if (!getItems().isEmpty()) {
            sb.append(" ").append(getItems().stream()
                    .map(Item::toPrettyString)
                    .collect(Collectors.joining(", ", "{", "}")));
        }

        if (!Utils.isBlank(getDescription())) {
            sb.append(" \"").append(getDescription()).append("\"");
        }
        return sb.toString();
    }

    public String toPrettyString(Book book) {
        final String accountName = book.getChartOfAccounts()
                .findFullNameByCode(getAccount().getCode())
                .orElse(getAccount().getName());
        return toPrettyString(accountName);
    }

    public String toPrettyString() {
        return toPrettyString(getAccount().getName()
                + " (" + getAccount().getCode() + ")");
    }
}
